知识 - 请求日志输出
前言
在 Spring Boot 应用中,实现接口请求日志记录功能,能够记录包括请求方法、接口路径及请求参数等核心信息,并提供灵活的开关配置。
实现逻辑是利用 AOP 切面在 Controller 进行切入,获取请求的数据并打印处理。
实现
依赖
xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>枚举
java
@Getter
@RequiredArgsConstructor
public enum RequestLogLevelEnum {
/**
* No logs.
*/
NONE(0),
/**
* Logs request and response lines.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC(1),
/**
* Logs request and response lines and their respective headers.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS(2),
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY(3);
/**
* 级别
*/
private final int level;
/**
* 请求日志配置前缀
*/
public static final String REQ_LOG_PROPS_PREFIX = "controller.log";
/**
* 控制台日志是否启用
*/
public static final String CONSOLE_LOG_ENABLED_PROP = "controller.log.console.enabled";
/**
* 当前版本 小于和等于 比较的版本
*
* @param level LogLevel
* @return 是否小于和等于
*/
public boolean lte(RequestLogLevelEnum level) {
return this.level <= level.level;
}
}配置项
java
@Getter
@Setter
@ConfigurationProperties(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
public class RequestLogProperties {
/**
* 日志级别配置,默认:BODY
*/
private RequestLogLevelEnum level = RequestLogLevelEnum.BODY;
}切面
java
@Slf4j
@Aspect
@Configuration
@AllArgsConstructor
@ConditionalOnClass(LogAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX + ".enabled", havingValue = "true", matchIfMissing = true)
public class RequestLogAspect {
private final RequestLogProperties properties;
/**
* AOP 环切 控制器 R 返回值
* Response:响应类
*
* @param point JoinPoint
* @return Object
* @throws Throwable 异常
*/
@Around(
"execution(!static cn.youngkbt.core.http.Response *(..)) && " +
"(@within(org.springframework.stereotype.Controller) || " +
"@within(org.springframework.web.bind.annotation.RestController))"
)
public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
RequestLogLevelEnum level = properties.getLevel();
// 不打印日志,直接返回
if (RequestLogLevelEnum.NONE == level) {
return point.proceed();
}
HttpServletRequest request = WebUtil.getRequest();
String requestUrl = Objects.requireNonNull(request).getRequestURI();
String requestMethod = request.getMethod();
// 构建成一条长 日志,避免并发下日志错乱
StringBuilder beforeReqLog = new StringBuilder(300);
// 日志参数
List<Object> beforeReqArgs = new ArrayList<>();
beforeReqLog.append("\n\n================ Request Start ================\n");
// 打印路由
beforeReqLog.append("===> {}: {}");
beforeReqArgs.add(requestMethod);
beforeReqArgs.add(requestUrl);
// 打印请求参数
logIngArgs(point, beforeReqLog, beforeReqArgs);
// 打印请求 headers
logIngHeaders(request, level, beforeReqLog, beforeReqArgs);
beforeReqLog.append("================ Request End ================\n");
// 打印执行时间
long startNs = System.nanoTime();
log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
// aop 执行后的日志
StringBuilder afterReqLog = new StringBuilder(200);
// 日志参数
List<Object> afterReqArgs = new ArrayList<>();
afterReqLog.append("\n\n================ Response Start ================\n");
try {
Object result = point.proceed();
// 打印返回结构体
if (RequestLogLevelEnum.BODY.lte(level)) {
afterReqLog.append("===Result=== {}\n");
afterReqArgs.add(JacksonUtil.toJsonStr(result));
}
return result;
} finally {
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
afterReqLog.append("<=== {}: {} ({} ms)\n");
afterReqArgs.add(requestMethod);
afterReqArgs.add(requestUrl);
afterReqArgs.add(tookMs);
afterReqLog.append("================ Response End ================\n");
log.info(afterReqLog.toString(), afterReqArgs.toArray());
}
}
/**
* 激励请求参数
*
* @param point ProceedingJoinPoint
* @param beforeReqLog StringBuilder
* @param beforeReqArgs beforeReqArgs
*/
public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
// 请求参数处理
final Map<String, Object> paraMap = new HashMap<>(16);
// 一次请求只能有一个 request body
Object requestBodyValue = null;
for (int i = 0; i < args.length; i++) {
// 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// PathVariable 参数跳过
PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
if (pathVariable != null) {
continue;
}
RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
String parameterName = methodParam.getParameterName();
Object value = args[i];
// 如果是body的json则是对象
if (requestBody != null) {
requestBodyValue = value;
continue;
}
// 处理 参数
if (value instanceof HttpServletRequest) {
paraMap.putAll(((HttpServletRequest) value).getParameterMap());
continue;
} else if (value instanceof WebRequest) {
paraMap.putAll(((WebRequest) value).getParameterMap());
continue;
} else if (value instanceof HttpServletResponse) {
continue;
} else if (value instanceof MultipartFile) {
MultipartFile multipartFile = (MultipartFile) value;
String name = multipartFile.getName();
String fileName = multipartFile.getOriginalFilename();
paraMap.put(name, fileName);
continue;
} else if (value instanceof List) {
List<?> list = (List<?>) value;
AtomicBoolean isSkip = new AtomicBoolean(false);
for (Object o : list) {
if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
isSkip.set(true);
break;
}
}
if (isSkip.get()) {
paraMap.put(parameterName, "此参数不能序列化为json");
continue;
}
}
// 参数名
RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class);
String paraName = parameterName;
if (requestParam != null && StringUtil.hasText(requestParam.value())) {
paraName = requestParam.value();
}
if (value == null) {
paraMap.put(paraName, null);
} else if (ClassUtil.isPrimitiveOrWrapper(value.getClass())) {
paraMap.put(paraName, value);
} else if (value instanceof InputStream) {
paraMap.put(paraName, "InputStream");
} else if (value instanceof InputStreamSource) {
paraMap.put(paraName, "InputStreamSource");
} else if (JacksonUtil.canSerialize(value)) {
// 判断模型能被 json 序列化,则添加
paraMap.put(paraName, value);
} else {
paraMap.put(paraName, "此参数不能序列化为json");
}
}
// 请求参数
if (paraMap.isEmpty()) {
beforeReqLog.append("\n");
} else {
beforeReqLog.append(" Parameters: {}\n");
beforeReqArgs.add(JacksonUtil.toJsonStr(paraMap));
}
if (requestBodyValue != null) {
beforeReqLog.append("====Body===== {}\n");
beforeReqArgs.add(JacksonUtil.toJsonStr(requestBodyValue));
}
}
/**
* 记录请求头
*
* @param request HttpServletRequest
* @param level 日志级别
* @param beforeReqLog StringBuilder
* @param beforeReqArgs beforeReqArgs
*/
public void logIngHeaders(HttpServletRequest request, RequestLogLevelEnum level,
StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
// 打印请求头
if (RequestLogLevelEnum.HEADERS.lte(level)) {
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String headerName = headers.nextElement();
String headerValue = request.getHeader(headerName);
beforeReqLog.append("===Headers=== {}: {}\n");
beforeReqArgs.add(headerName);
beforeReqArgs.add(headerValue);
}
}
}
}容器装配
主要加载 RequestLogProperties 配置类。
java
@AutoConfiguration
@EnableConfigurationProperties({RequestLogProperties.class})
public class LogAutoConfiguration {
}Spring Boot 3.x 需要在 resource 下建立 META-INF/spring 路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为
java
cn.youngkbt.log.config.LogAutoConfiguration这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。